﻿#region directives

using System;
using System.Security.Cryptography;
using System.Text;
using System.Xml.Linq;
using ToodleDo;
using ToodleDo.Client;
using Toodledo.Model;
using Toodledo.Model.API;

#endregion

namespace Toodledo.Client
{
    public partial class Session
    {
        #region ActionTypes enum

        public enum ActionTypes
        {
            AccountLookup,
            GetTasks,
            AddFolder,
            GetToken,
            EditFolder,
            AddTask,
            DeleteFolder,
            EditTask,
            DeleteTask,
            GetDeleted,
            GetFolders,
            GetAccountInfo
        }

        #endregion

        public Session()
        {
        }

        private Session(string userid, string password, string appid, string apptoken)
        {
            UserId = userid;
            Password = password;
            AppToken = apptoken;
            AppId = appid;
            Token = ((IAuthentication) this).GetToken(userid, appid, AppToken);
            Account = ((IAuthentication) this).GetAccountInfo();
        }

        public string AppId { get; set; }
        public string AppToken { get; set; }
        public string Token { get; set; }
        public string UserId { get; set; }
        public string Password { get; set; }
        public Account Account { get; set; }

        #region Communications

        private static bool _isRevalidating;

        private static XDocument ExecuteRequest(ActionTypes type, string parameters, string apptoken, string token,
                                                string password)
        {
            string key = GetKey(token, password, apptoken);
            bool post = false;
            const string site = @"http://api.toodledo.com/2/";
            string method = null;
            switch (type)
            {
                case ActionTypes.AccountLookup:
                    method = "account/lookup.php?";
                    key = String.Empty;
                    break;
                case ActionTypes.GetTasks:
                    method = "tasks/get.php?";
                    break;
                case ActionTypes.AddFolder:
                    method = "folders/add.php?";
                    break;
                case ActionTypes.GetToken:
                    method = "account/token.php?";
                    key = String.Empty;
                    break;
                case ActionTypes.EditFolder:
                    method = "folders/edit.php?";
                    break;
                case ActionTypes.AddTask:
                    method = "tasks/add.php";
                    post = true;
                    break;
                case ActionTypes.DeleteFolder:
                    method = "folders/delete.php?";
                    break;
                case ActionTypes.EditTask:
                    method = "tasks/edit.php";
                    post = true;
                    break;
                case ActionTypes.DeleteTask:
                    method = "tasks/delete.php?";
                    break;
                case ActionTypes.GetDeleted:
                    method = "tasks/deleted.php?";
                    break;
                case ActionTypes.GetFolders:
                    method = "folders/get.php?";
                    break;
                case ActionTypes.GetAccountInfo:
                    method = "account/get.php?";
                    break;
                default:
                    throw new ArgumentOutOfRangeException("type");
            }
            if (post)
            {
                string package = key.Replace(";", "&") + parameters + "&f=xml";
                Logger.InvokeMessageEvent(package, Logger.MessageType.Tracking);
                return Connection.Post(site + method, package);
            }
            else
            {
                method += "f=xml;" + key + parameters;
                //var url = String.Format("http://api.toodledo.com/2/{0}{1};{2}", method, key, parameters);
                string query = site + method; //+key??
                Logger.InvokeMessageEvent(query, Logger.MessageType.Tracking);
                return Connection.Get(query);
            }
        }

        private XDocument SendRequest(ActionTypes type, string parameters)
        {
            XDocument results = ExecuteRequest(type, parameters, AppToken, Token, Password);
            if (results.Root == null)
                Logger.InvokeMessageEvent("Invalid XML message from Toodledo", Logger.MessageType.Error);
            else if (results.Root.Name == "error")
            {
                int id = ReadAttribute<int>(results, "id");

                if (id == 2)
                {
                    if (!_isRevalidating)
                    {
                        _isRevalidating = true;
                        // Our session has expired, so get a new token.
                        Token = ((IAuthentication) this).GetToken(UserId, AppId, AppToken);
                        //Token = ((IAuthentication) this).GenerateToken(UserId, AppId);
                        Cache.Set(this, UserId);
                        results = SendRequest(type, parameters);
                        _isRevalidating = false;
                        return results;
                    }
                }
                Logger.InvokeMessageEvent(String.Format("Error \"{0}\"", ReadElement<string>(results)),
                                          Logger.MessageType.Error);
                return null;
            }
            return results;
        }

        private XDocument SendRequest(ActionTypes type)
        {
            return SendRequest(type, String.Empty);
        }

        private ItemType ReadElement<ItemType>(XDocument document)
        {
            return ReadElement<ItemType>(document.Root);
        }

        private ItemType ReadElement<ItemType>(XDocument document, string name)
        {
            if (document.Root == null)
                return default(ItemType);
            return ReadElement<ItemType>(document.Root, name);
        }

        private ItemType ReadElement<ItemType>(XElement element, string name)
        {
            return ReadElement<ItemType>(element.Element(XName.Get(name, String.Empty)));
        }

        public DateTime ReadElement(XDocument element, string name)
        {
            int secs = ReadElement<int>(element, name);
            DateTime gmt = new DateTime(1970, 1, 1, 0, 0, 0);
            return gmt.AddSeconds(secs);
        }

        public ItemType ReadElement<ItemType>(XElement element)
        {
            string value = (element == null) ? String.Empty : element.Value;
            return (ItemType) Convert.ChangeType(value, typeof (ItemType));
        }

        public ItemType ReadAttribute<ItemType>(XDocument document, string name)
        {
            if (document.Root == null)
                return default(ItemType);
            return ReadAttribute<ItemType>(document.Root, name);
        }

        public ItemType ReadAttribute<ItemType>(XElement element, string name)
        {
            XAttribute attribute = element.Attribute(XName.Get(name, String.Empty));
            string value = (attribute == null) ? String.Empty : attribute.Value;
            return (ItemType) Convert.ChangeType(value, typeof (ItemType));
        }

        private static string FormatArgument(bool value, string name)
        {
            return String.Format("{0}={1};", name, (value) ? 1 : 0);
        }

        private static string FormatArgument(int? value, string name)
        {
            return (!value.HasValue) ? String.Empty : String.Format("{0}={1};", name, value);
        }

        private static string FormatArgument(int value, string name)
        {
            return String.Format("{0}={1};", name, value);
        }

        private static string FormatArgument(string value, string name)
        {
            return (String.IsNullOrEmpty(value)) ? String.Empty : String.Format("{0}={1};", name, EncodeString(value));
        }


        private static string FormatArgument(DateTime? value, string name)
        {
            return (!value.HasValue) ? String.Empty : String.Format("{0}={1};", name, value.Value);
        }

        private static string FormatArgument(DateTime value, string name)
        {
            return (value == DateTime.MinValue) ? String.Empty : String.Format("{0}={1};", name, value);
        }

        private static string FormatArgument(TimeSpan value, string name)
        {
            return (value == TimeSpan.Zero) ? String.Empty : String.Format("{0}={1};", name, value);
        }

        private static string FormatArgument(Item value, string name)
        {
            return (value == null) ? String.Empty : String.Format("{0}={1};", name, value.Id);
        }

        private static string FormatArgumentPost(string value, string name)
        {
            return (String.IsNullOrEmpty(value))
                       ? String.Empty
                       : String.Format("\"{0}\":\"{1}\"", name, EncodeString(value));
        }

        private static string FormatArgumentPost(int value, string name)
        {
            return String.Format("\"{0}\":\"{1}\"", name, value);
        }

        #endregion Communications

        public static Session Create(string user, string password, string appid, string apptoken)
        {
            string userid = (user.Contains("@") ? GetUserId(user, password, appid, apptoken) : user);            
            return userid != null ? (Cache.Set(new Session(userid, password, appid, apptoken), userid)) : null;
        }

        private static string GetUserId(string user, string password, string appid, string apptoken)
        {            
            string sig = MD5(user + apptoken);
            string parameters =
                FormatArgument(appid, "appid") +
                FormatArgument(sig, "sig") +
                FormatArgument(user, "email") +
                FormatArgument(password, "pass");
            XDocument results = ExecuteRequest(ActionTypes.AccountLookup, parameters, apptoken, null, password);           
            Logger.InvokeMessageEvent("userid requested", Logger.MessageType.Tracking);
            try
            {
                if (results != null)
                {
                    string userid = results.Element(XName.Get("userid", "")).Value;
                    return userid;
                }
            }
            catch (Exception e)
            {
                Logger.InvokeMessageEvent(e.Message, Logger.MessageType.Error);
            }
            return null;
        }

        private static string GetKey(string token, string password, string apptoken)
        {
            // key = md5( md5(password)+token+myuserid )
            string key = MD5(MD5(password) + apptoken + token);
            return FormatArgument(key, "key");
        }

        private static string MD5(string input)
        {
            MD5CryptoServiceProvider x = new MD5CryptoServiceProvider();
            byte[] bs = Encoding.UTF8.GetBytes(input);
            bs = x.ComputeHash(bs);
            StringBuilder s = new StringBuilder();
            foreach (byte b in bs)
            {
                s.Append(b.ToString("x2").ToLower());
            }
            string password = s.ToString();
            return password;
        }
    }
}